Skip to content

fix(report): report eval, generate and pipeline use one paired arm summary (#229 P1-3) - #250

Merged
xaleryb merged 7 commits into
intel:mainfrom
napetrov:fix/report-eval-generate-agree
Oct 29, 2026
Merged

xaleryb merged 7 commits into
intel:mainfrom
napetrov:fix/report-eval-generate-agree

Conversation

@napetrov

Copy link
Copy Markdown
Contributor

Summary

Item #229 P1-3. report eval, report generate and the pipeline run summary read one eval file and printed different arm means and deltas. Each now calls one function, eval.stats.paired_summary.

eval/stats.py
  paired_summary(evaluations) -> PairedSummary      (complete pairs only, unrounded)
    ├── report/eval_report.py   stats()  -> Summary, Breakdown, Difficulty tables
    ├── report/generator.py     _compute_stats(), _arm_means()  -> Overall, topic, source
    └── orchestrator/pipeline.py step-5 summary (printed + steps.evaluation.summary)
-  report generate: with = mean(every scored context answer)
-                   without = mean(every scored baseline answer)
-                   delta = mean(stored per-record delta)
+  paired_summary: pairs = questions scored in BOTH arms
+                  with, without = means over pairs
+                  delta = with - without (= mean of paired differences)
+                  unpaired = total - pairs (counted, printed)

Rounding: values are unrounded inside PairedSummary and rounded once for display.

Paired definition (why). The delta is a per-question comparison, so both arm means must cover the same questions. The trust gate (gate/trust_gate.py) and the model-compare summaries already use complete pairs. Averaging each arm over all of its questions gives two means over different sets whose difference is not the printed delta (fixture: 73.3 − 50.0 printed as delta 10.0).

Evidence

Regression test: tests/test_report_arm_agreement.py::test_report_eval_and_report_generate_print_the_same_arm_means_and_delta. Fixture: q1 80/60, q2 40/40, q3 context 100 with no baseline (unpaired).

  • Before (old code, pytest tests/test_report_arm_agreement.py):
E       AssertionError: assert {'with': 73.3...'delta': 10.0} == {'with': 60.0...'delta': 10.0}
E         Differing items:
E         {'with': 73.3} != {'with': 60.0}
1 failed
  • Before, pipeline summary (old code, tests/test_pipeline_summary.py, extra one-arm question q4 context 100):
E         {'with_avg': 80.0} != {'with_avg': 70.0}
1 failed
  • After: the same tests pass; tests/test_report_arm_agreement.py and tests/test_pipeline_summary.py pass (6 passed).
  • Full suite on the rebased branch (on intel/main a2b787a): 8981 passed, 4 skipped. ruff check clean, mypy clean.

Consumers checked by grep: report/, orchestrator/pipeline.py, commands/benchmark.py (reads the pipeline summary with_avg), docs/quickstart.md (step 5 wording updated), docs/benchmarking-and-comparison.md, docs/methodology.md. report/model_compare.py already averages paired deltas and is unchanged.

Internal review: two rounds (author-applied lenses; the subagent review tool was not available in this session). Round 1 found the pipeline summary still used the per-arm formula; fixed in this PR with a test.

Merge Danger

Door: two-way. Code only; no stored format changes. The JSON stats block gains total_questions, paired_questions, unpaired_questions keys and the pipeline summary keys are unchanged. Reverting restores the old numbers.

Numbers change (say which, and how):

  • report generate (and the pipeline report): Overall context / baseline means, min/max, topic and source means, and the Count cell change for any eval file with an unpaired question (scored in one arm only). For a file the judge wrote, the delta does not change, because the stored delta already was the paired difference. Files where every question is paired do not change.
  • pipeline run summary (printed line, steps.evaluation.summary, and benchmark run averages read from it): same rule, complete pairs.
  • report eval: arm means print as before. The Delta is the unrounded difference rounded once, so it can move by 0.1 (rounding probe: about 22% of random cases differ by 0.1). The Count cell shows unpaired questions and a note explains the means.

Blast radius: report output and the pipeline summary for files with unpaired questions. Results recorded before this change keep their old numbers in their own reports; rerun report generate on an existing eval file to get the new ones. No judge, answer or eval-file writes change.

Out of scope, found and not changed: eval_report win-rate and degradation percentages divide by all questions while numerators are over pairs; generator top/bottom delta lists read the stored per-record delta (equal for judge-written files); Key Observations thresholds use rounded display values.

Refs #229

🤖 Generated with Claude Code

@codecov-commenter

codecov-commenter commented Oct 27, 2026 •

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 93.04%. Comparing base (e2147b1) to head (fcc02f2).
⚠️ Report is 4 commits behind head on main.
❗ Your organization needs to install the Codecov GitHub app to enable full functionality.

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #250      +/-   ##
==========================================
+ Coverage   92.90%   93.04%   +0.14%     
==========================================
  Files         265      265              
  Lines       33460    33494      +34     
  Branches     5608     5612       +4     
==========================================
+ Hits        31086    31165      +79     
+ Misses       1568     1538      -30     
+ Partials      806      791      -15     
Files with missing lines Coverage Δ
dokimi/dokimi/eval/stats.py 96.20% <100.00%> (+0.42%) ⬆️
dokimi/dokimi/orchestrator/pipeline.py 83.83% <100.00%> (-0.19%) ⬇️
dokimi/dokimi/report/eval_report.py 86.97% <100.00%> (+1.53%) ⬆️
dokimi/dokimi/report/generator.py 91.92% <100.00%> (+4.26%) ⬆️

... and 6 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@antonwolfy antonwolfy left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review at ac50fc0. The change is right: paired_summary is one definition, the three consumers now agree on arm means and delta, and the changelog and quickstart match the code. Worth fixing before merge: a report page that still contradicts its own Overall table, and test gaps in the grouped tables.

What I ran, on Linux at ac50fc0:

  • make ci-local: 26 passed, 7 skipped, 2 failed. The two failures are the gitleaks download (the proxy returned 504) and the scan that needs it, not this PR; a direct gitleaks run over the PR's commit found no leaks. Test run and changed-line coverage steps pass.
  • Reproductions for the findings, with the LLM transport mocked.
  • A mutation run of 38 broken copies of the new code against the report, generator, stats, pipeline and record tests: 15 survive.

Findings outside the diff

  1. Win rate and degradation rate in Key Observations still divide by all questions. report/eval_report.py:473-476 uses all_stats["count"]. With q1 80/60, q2 40/50, q3 context-only and q4 unscored, the Summary row reads 4 (2 unpaired) | 60.0 | 55.0 | +5.0 and the Key Observations read Win rate: 25% ... (1/4); on the pairs it is 1/2. This predates the PR and the description lists it as out of scope, so it is not a blocker. Using all_stats["valid"] as the denominator would fix it.

Checked, no issues

  • Delta claim: eval/judge.py:399 writes delta = with - without, so the delta mean over pairs equals the old stored-delta mean for judge-written files, as the changelog says.
  • Bad scores: NaN, inf, bool, None, string and one-arm scores through paired_summary all end up unpaired, with no exception.
  • Pipeline consumers: commands/benchmark.py and commands/orchestrate.py handle a missing summary and still read with_avg, without_avg and delta_avg.
  • Repo rules: a changelog fragment is present, no schema or nav change is needed, and the new tests write only under tmp_path.

Comment thread dokimi/dokimi/report/generator.py Resolved
@xaleryb

xaleryb commented Oct 27, 2026

Copy link
Copy Markdown
Contributor
  • Ran: head merged onto current main; full test suite, ruff, mypy and bandit are clean. Also ran report generate, report eval and the dashboard aggregator on eval files that have unpaired questions.
  • The dashboard still averages each arm over every scored question. Input: 8 complete pairs plus 1 question scored only with context. Observed: report generate shows 63.8 / 57.5 / +6.2, and the dashboard snapshot shows 64.4 / 57.5 / +6.2 (on main they matched). Expected: ProductSnapshot and _compute_by_source go through paired_summary, keeping the per-arm fallback when nothing is paired. If not that, then the stats.py docstring ("Every report that prints arm means or a delta calls it") and the changelog should name the dashboard and the model-compare arm means as exceptions. The dashboard status band reads avg_with_docs, so this is your call.
  • Pushed: f063300 adds a test with pairs (80,70), (81,70), (81,71). It pins +10.3 in both reports, the Breakdown Total row, delta min/max and the improvement counts. Without it, putting back the rounded-difference delta passed every test.

@antonwolfy antonwolfy left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review at f063300. The only change since my last review is a new test that pins the unrounded-mean delta (+10.3 against +10.4 from rounded means), and it does that well. Both of my earlier points are still open, so I would not merge yet.

What I ran, on Linux at f063300:

  • make ci-local: 28 passed, 0 failed, 7 skipped.
  • A mutation run of the grouped-table and paired_summary code against test_report_arm_agreement.py, test_pipeline_summary.py and test_record_readers.py: four of five mutants pass all three files.

Still open from my earlier review:

  • dokimi/dokimi/report/generator.py:316: _top_performers and _bottom_performers (generator.py:170-194, outside this diff) still rank every context-scored question, unpaired ones included, while the Max row above them is over the complete pairs. Unchanged since the first review.
  • dokimi/tests/test_report_arm_agreement.py:70: the new test closes the rounded-difference mutant only. These still pass all three suites: _arm_means with round(s.with_mean + 7, 1); _arm_means with unpaired forced to 0; _count_cell replaced by the plain count in the cluster and source rows; without_min and without_max swapped in paired_summary. The grouped tables (clusters, sources_summary) have no assertion at all.

@xaleryb's question on whether the dashboard should use paired_summary too, or be named as an exception, waits for the author.

@napetrov
napetrov force-pushed the fix/report-eval-generate-agree branch from f063300 to e3a08ef Compare October 28, 2026 07:03

@antonwolfy antonwolfy left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review at e3a08ef. The top/bottom performer lists now rank the complete pairs, so the page no longer contradicts its Overall table. One test gap from my earlier review is still open, and I would not merge before it is closed.

What I ran, on a Linux review host at e3a08ef:

  • make ci-local: 28 passed, 0 failed, 7 skipped. The test run and the changed-line coverage step pass.
  • A mutation run of 28 broken copies of the new code against test_report_arm_agreement.py, test_pipeline_summary.py and test_record_readers.py: 24 are killed, 4 survive (under the open thread below). The grouped-table, paired_summary min/max and eval_report.stats mutants from my earlier review are now all killed.
  • Reproductions with the LLM transport mocked for one-arm-only files and stale stored deltas.

Still open from my earlier review:

  • dokimi/tests/test_report_arm_agreement.py:70: the pipeline summary is the one consumer whose rounding is still unpinned. Mutation check: delta_avg in pipeline.py:390 computed as round(round(with_mean, 1) - round(without_mean, 1), 1), and without_avg (pipeline.py:389) on the old per-arm formula, both pass the three suites (118 passed). The test_pipeline_summary.py fixture has means of 70.0 and 45.0 and only a context-only unpaired question (q4). Add a pair such as 60.04 / 50.06 there, and a baseline-only question. Also, _count_cell replaced by the plain count in the Generated (dynamic), Golden (static), by-type and by-difficulty rows of eval_report.py (lines 286, 287, 341, 356) still passes; only the All row is asserted.

I agree with @xaleryb that the dashboard still averages each arm separately. New evidence: report/model_compare.py:935-951 _run_summary filters with_avg and without_avg independently while delta_avg comes from the pairs, so model-compare has the same mismatch. That question waits for the author, as does the docstring claim in stats.py that every report calls paired_summary.

Checked, no issues

  • Top/bottom lists: _top_performers and _bottom_performers now go through _complete_pairs, and test_top_and_bottom_performers_rank_the_complete_pairs_like_the_overall_max pins it.
  • Pipeline consumers: commands/benchmark.py:274 and commands/orchestrate.py still read with_avg, without_avg, delta_avg; a file with no pairs falls to the no-summary branch.
  • Repo rules: changelog fragment present, no schema or nav change needed, no generated artifacts, new tests write only under tmp_path.

@vchamarthi vchamarthi left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: Needs fixes before merge

report eval, report generate and the pipeline run summary now agree on arm means and delta through one function, eval.stats.paired_summary. The fix for the earlier top/bottom-performers mismatch is confirmed in; two test gaps raised by earlier review are still open.

Blocking

  • dokimi/dokimi/report/eval_report.py:286: the Generated, Golden, by-type and by-difficulty rows (also lines 287, 341, 356) have no test with an unpaired question, so a regression in _count_cell there passes CI. Add one grouped-table test with an unpaired question.
  • dokimi/dokimi/orchestrator/pipeline.py:390: the delta rounding order is untested; the fixture means have no fractional part, so both orders give the same result. Use fractional means in test_pipeline_summary.py.

Questions

  • the dashboard summary still averages each arm separately while its delta comes from the paired differences, raised by another reviewer and still waiting on the author.

Earlier review threads

Status Count Threads
Fixed, with a test 1 top/bottom performers now rank the complete pairs
Still open 2 grouped-table test gap; dashboard arm-average question

CI

Check Result Cause
test suite pass 2 reruns alone, both pass on this PR and on its base: pre-existing, load-induced
dependency audit fail pre-existing vulnerable pin, unrelated to this PR's files
package build / docs build / static checks pass -

f'| Generated (dynamic) | {dyn_stats["count"]} | {dyn_stats["with_avg"]} | {dyn_stats["without_avg"]} | **{dyn_stats["delta_avg"]:+}** |',
f'| Golden (static) | {static_stats["count"]} | {static_stats["with_avg"]} | {static_stats["without_avg"]} | **{static_stats["delta_avg"]:+}** |',
f'| **All** | {_count_cell(all_stats)} | {all_stats["with_avg"]} | {all_stats["without_avg"]} | **{all_stats["delta_avg"]:+}** |',
f'| Generated (dynamic) | {_count_cell(dyn_stats)} | {dyn_stats["with_avg"]} | {dyn_stats["without_avg"]} | **{dyn_stats["delta_avg"]:+}** |',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

blocking (still open from @antonwolfy): the Generated (dynamic), Golden (static), by-type and by-difficulty rows (also lines 287, 341, 356) call _count_cell the same as the All row, but no test exercises them with an unpaired question.

Mutating _count_cell to drop its unpaired branch still passes the full test_report_arm_agreement.py suite, because none of those fixtures has an unpaired question in a difficulty/dyn/static split that differs from the All row's count.

Suggested fix: add a fixture with an unpaired question in one of these groups and assert the rendered count cell, as the test already does for the All row.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Covered at a92f424, no code change. test_eval_report_group_rows_count_their_unpaired_questions (03efe3c, tests/test_report_arm_agreement.py) has an unpaired question in the dynamic and the golden groups and asserts the (N unpaired) cell on the Generated (dynamic) and Golden (static) Summary rows, and on the by-type and by-difficulty rows. Dropping the unpaired branch of _count_cell at each of those call sites (lines 286, 287, 341, 356) fails that test.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Covered in 03efe3c: test_eval_report_group_rows_count_their_unpaired_questions has unpaired questions in the dynamic and golden groups and asserts their count cells. Removing the unpaired branch at any of those call sites fails it.

"delta_avg": round(sum(deltas) / len(deltas), 1)
"with_avg": round(summary.with_mean, 1),
"without_avg": round(summary.without_mean, 1),
"delta_avg": round(summary.delta_mean, 1)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

blocking (still open from @antonwolfy): this is the only one of the three consumers whose rounding order (round-of-the-unrounded-difference vs difference-of-the-rounded-means) is untested.

The test_pipeline_summary.py fixture has mean values with no fractional component, so both rounding orders give the same result. Replacing this line with round(round(summary.with_mean, 1) - round(summary.without_mean, 1), 1) still passes every test in test_pipeline_summary.py and test_report_arm_agreement.py.

Suggested fix: add a fixture with means like 70.04/50.06 so the rounding order is pinned, as test_report_arm_agreement.py already does for report eval and report generate.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Covered at a92f424, no code change. test_summary_rounds_the_delta_from_the_unrounded_means (03efe3c, tests/test_pipeline_summary.py) uses means 60.04 / 50.06: the delta of the unrounded means is 10.0, and the difference of the rounded means is 9.9. Replacing line 390 with round(round(summary.with_mean, 1) - round(summary.without_mean, 1), 1) fails that test, and so does dropping the round.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Covered in 03efe3c: the pipeline summary test uses means 60.04 and 50.06 and expects a delta of 10.0. Computing the delta from the rounded means fails it.

napetrov and others added 3 commits October 28, 2026 19:52
… and generate

report eval and report generate read the same eval file and printed different
arm means and deltas. report eval averaged the arms over the complete pairs;
report generate averaged each arm over every question scored in it, so the
context and baseline means could cover different questions and their
difference was not the delta printed beside them. The pipeline run summary
had the same per-arm averages.

All of them now call eval.stats.paired_summary, which takes the complete pairs
only: the arm means, min/max and delta, and the improved/degraded counts. The
delta is the difference of the paired means, which equals the mean of the
paired deltas. The trust gate and the significance test already used complete
pairs. Unpaired questions are counted (paired_questions, unpaired_questions in
the JSON; a note and the Count cell in the Markdown), so none drops silently.

report eval also rounded each arm mean before subtracting. The delta is now
the unrounded difference rounded once for display, so it can move by 0.1.

Refs intel#229 (P1-3)

Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
The agreement fixture has arm means exact at one decimal, so putting back
the rounded-difference delta in report eval passed every test. Three pairs
with means 80.67/70.33 pin +10.3 in both reports, plus the Breakdown Total
row, the delta min/max and the improvement counts.

Signed-off-by: Alexander Rybkin <alexander.rybkin@intel.com>
The Overall Max covers only the questions scored in both arms, but the
top and bottom performer tables still ranked every context-arm answer,
so an unpaired answer could score above the Overall Max printed beside it.
Both tables now use the same complete pairs as the Overall statistics,
and the note under the Overall table says so.

The tests now also pin the grouped tables (topic and source): their
means, deltas, unpaired counts and the "(N unpaired)" cell, plus the
without-arm min/max and a delta that is not the difference of rounded
means.

Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
@napetrov
napetrov force-pushed the fix/report-eval-generate-agree branch from e3a08ef to e062773 Compare October 29, 2026 02:53
The pipeline summary's delta is the difference of the unrounded means, and
its baseline mean covers the complete pairs only. The eval report's Summary,
breakdown and difficulty rows print the unpaired count beside the question
count. Each of these had a surviving mutant. The new assertions kill them.

Thread: PRRT_kwDOVBg7g86wpmIH
Review: PRR_kwDOVBg7g88AAAABUqK69Q#1

Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
The docstring said every report that prints arm means or a delta calls
paired_summary. The eval report, the report generator and the pipeline
summary do. The dashboard and the model-compare summary still average each
arm separately, which is follow-up work because it changes their numbers.

Thread: IC_kwDOVBg7g88AAAABgAwfHA.a
Review: PRR_kwDOVBg7g88AAAABUdomSg: already fixed in 68d90f23 and f09eeb77

Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
The win rate and the degradation rate in Key Observations divided by every
question, but their numerators count only the complete pairs. An unpaired
question therefore lowered both rates. They now divide by the complete pairs,
the same set the Overall delta uses.

Thread: PRR_kwDOVBg7g88AAAABUbZiqA

Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
@napetrov

Copy link
Copy Markdown
Contributor Author

Replies to the reviews by antonwolfy and to the issue comment by xaleryb. Each section answers one review or comment.

Review at ac50fc0

Answered, fixed in a92f424. The win rate and the degradation rate in Key Observations now divide by the complete pairs. Before, an unpaired question lowered both rates while the numerators counted only pairs. test_eval_report_win_and_degradation_rates_divide_by_the_complete_pairs pins 50% (2/4) and 25% on a fixture with four pairs and two unpaired questions; the old denominator (6) gave 33% and 17%. This changes the Key Observations numbers of the eval report.

Review at f063300

Already fixed in e062773 (the top and bottom performer tables rank the complete pairs) and in 03efe3c (the pipeline summary rounding and the grouped-table count cells are pinned by tests).

Review at e3a08ef

Fixed in 03efe3c. test_eval_report_group_rows_count_their_unpaired_questions asserts the (N unpaired) cells of the All, Generated (dynamic) and Golden (static) rows. The pipeline delta rounding is pinned by test_summary_rounds_the_delta_from_the_unrounded_means.

Issue comment by xaleryb

a: Fixed in 94730e7. The paired_summary docstring in eval/stats.py names its callers (the eval report, the report generator and the pipeline summary) and says that the dashboard and the model-compare summary still average each arm separately.

b: Not done in this PR. The dashboard and the model-compare arm means still average each arm separately. Changing them changes their reported numbers, so it is out of scope here.

@napetrov

Copy link
Copy Markdown
Contributor Author

Dashboard arm averages: not changed in this PR. dokimi/dashboard/aggregator.py:200-202 and :225-227 still average each arm over its own scored questions, and the paired_summary docstring (94730e7) says so. Switching them to the complete pairs changes the dashboard's reported numbers, so this stays as the reply to xaleryb's comment (b) describes. Question for the reviewer: should it move into this PR, or be a follow-up?

CI: the table in the review refers to e3a08ef. On a92f424 every check run is success or skipped.

The fragment did not say that the top/bottom performer tables and the
Key Observations win and degradation rates now count only the complete
pairs, nor that the dashboard and the model-compare summary keep the
per-arm means and so differ from report generate on unpaired files.

Signed-off-by: Alexander Rybkin <alexander.rybkin@intel.com>
@xaleryb

xaleryb commented Oct 29, 2026

Copy link
Copy Markdown
Contributor
  • Ran: head a92f424 alone and merged onto current main: full test suite, ruff, mypy, bandit. Re-ran every earlier input: the 8-pairs-plus-1-unpaired file, the win-rate fixture (now 1/2, 50%), and the top/bottom q3 case (no longer listed). Mutants: each reverts one fix (count cells at eval_report 286/287/341/356 and generator 411/429, pipeline delta rounding, top/bottom pairing, win/degradation denominators, the rounded-difference delta). Every one fails a test.
  • Dashboard / model-compare (reply to the question): keeping them for a follow-up is fine now that this is stated. Input: 8 pairs + 1 context-only. Observed: report generate 64.5 / 56.1, dashboard 65.1 / 56.1. A follow-up can route ProductSnapshot, _compute_by_source and model_compare._run_summary through paired_summary.
  • Pushed: fcc02f2 adds to the PR's fragment the top/bottom table change, the new win/degradation denominators, and the fact that the dashboard and report model-compare still use per-arm means.

@xaleryb xaleryb left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved at fcc02f2: every point from our review is fixed at this head or left as a named follow-up, the full suite, ruff, mypy and bandit pass on Linux, and CI is green.

@xaleryb
xaleryb merged commit f0d98a7 into intel:main Oct 29, 2026
17 checks passed
antonwolfy added a commit to antonwolfy/dokimi that referenced this pull request Oct 29, 2026
The same point as the previous commit (the review of intel#250: an arm mean
over its own scored questions beside a delta over the pairs), at its
other site in `report model-compare`: each By Difficulty row averaged
the context arm over every context-scored question in the bucket, while
the delta beside it covers the pairs only. On a bucket with q1 80/60,
q2 40/40 and q3 100/- the row printed 73.3 | +10.0; it now prints
60.0 | +10.0, the run summary's numbers on the same questions.

- report/model_compare.py: _difficulty_cells takes the context mean and
  the delta from _run_summary, so a bucket follows the overall summary's
  rule: with pairs, the pair means; context arm only, its context mean
  and no delta; both arms scored but no pair, no mean. A bucket with no
  context score still shows the baseline, as before.
- The model-compare docs and the unreleased fragment name the
  difficulty rows; the goldens (the By Difficulty context cell of eight
  sections), the growth baseline of model_compare.py and its count in
  the records guard (one condition-key read fewer) are lowered.

Signed-off-by: Anton Volkov <antonwolfy@gmail.com>
antonwolfy added a commit that referenced this pull request Oct 29, 2026
* fix(dashboard, report): average both arms over the complete pairs

Settles the point the review of #250 left for a follow-up: the dashboard
and the `report model-compare` run summary averaged each arm over its own
scored questions, while `report generate`, `report eval` and the pipeline
average both over the questions scored in both arms (paired_summary). On
an eval file with q1 80/60, q2 40/40 and q3 100/- the dashboard and
model-compare printed 73.3 / 50.0 beside a +10.0 delta; `report generate`
prints 60.0 / 50.0 / +10.0.

- dashboard/aggregator.py: the snapshot and each question-source group
  take both arm means and the delta from the pairs (_arm_means). The
  delta is the difference of the pair means, as in paired_summary, not
  the mean of the stored per-question delta.
- report/model_compare.py: _run_summary averages with_avg and
  without_avg over the same pairs as delta_avg.
- In both, a run that scored one arm only keeps that arm's mean (the
  dashboard's doc_score and the baseline-only ranking read it); with
  both arms scored but no pair, neither mean is given.
- The stats module docstring, the unreleased #250 fragment, the
  quickstart and the model-compare docs say so; the model-compare
  goldens and the growth baseline of _load_snapshot are updated.

Signed-off-by: Anton Volkov <antonwolfy@gmail.com>

* fix(report): model-compare difficulty rows average over the pairs

The same point as the previous commit (the review of #250: an arm mean
over its own scored questions beside a delta over the pairs), at its
other site in `report model-compare`: each By Difficulty row averaged
the context arm over every context-scored question in the bucket, while
the delta beside it covers the pairs only. On a bucket with q1 80/60,
q2 40/40 and q3 100/- the row printed 73.3 | +10.0; it now prints
60.0 | +10.0, the run summary's numbers on the same questions.

- report/model_compare.py: _difficulty_cells takes the context mean and
  the delta from _run_summary, so a bucket follows the overall summary's
  rule: with pairs, the pair means; context arm only, its context mean
  and no delta; both arms scored but no pair, no mean. A bucket with no
  context score still shows the baseline, as before.
- The model-compare docs and the unreleased fragment name the
  difficulty rows; the goldens (the By Difficulty context cell of eight
  sections), the growth baseline of model_compare.py and its count in
  the records guard (one condition-key read fewer) are lowered.

Signed-off-by: Anton Volkov <antonwolfy@gmail.com>

---------

Signed-off-by: Anton Volkov <antonwolfy@gmail.com>
napetrov added a commit to napetrov/dokimi that referenced this pull request Oct 29, 2026
The summary averages the questions scored in both arms (paired_summary, intel#250),
so an unjudged retrieval miss is in none of the three numbers. The run result
now carries paired and unpaired, and the printed line names them.

Thread: PRRT_kwDOVBg7g86xgwon
Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
napetrov added a commit to napetrov/dokimi that referenced this pull request Oct 29, 2026
…ft unused

main's paired_summary (intel#250) replaced the BASELINE/CONTEXT/records reads in
run(); the merge kept this branch's import line, which ruff F401 rejects.

Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
napetrov added a commit to napetrov/dokimi that referenced this pull request Oct 29, 2026
…mmary (intel#229 P1-3) (intel#250)

* fix(report): average both arms over the complete pairs in report eval and generate

report eval and report generate read the same eval file and printed different
arm means and deltas. report eval averaged the arms over the complete pairs;
report generate averaged each arm over every question scored in it, so the
context and baseline means could cover different questions and their
difference was not the delta printed beside them. The pipeline run summary
had the same per-arm averages.

All of them now call eval.stats.paired_summary, which takes the complete pairs
only: the arm means, min/max and delta, and the improved/degraded counts. The
delta is the difference of the paired means, which equals the mean of the
paired deltas. The trust gate and the significance test already used complete
pairs. Unpaired questions are counted (paired_questions, unpaired_questions in
the JSON; a note and the Count cell in the Markdown), so none drops silently.

report eval also rounded each arm mean before subtracting. The delta is now
the unrounded difference rounded once for display, so it can move by 0.1.

Refs intel#229 (P1-3)

Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>

* test(report): pin the unrounded-mean delta in report eval and generate

The agreement fixture has arm means exact at one decimal, so putting back
the rounded-difference delta in report eval passed every test. Three pairs
with means 80.67/70.33 pin +10.3 in both reports, plus the Breakdown Total
row, the delta min/max and the improvement counts.

Signed-off-by: Alexander Rybkin <alexander.rybkin@intel.com>

* fix(report): rank the top/bottom performers over the complete pairs

The Overall Max covers only the questions scored in both arms, but the
top and bottom performer tables still ranked every context-arm answer,
so an unpaired answer could score above the Overall Max printed beside it.
Both tables now use the same complete pairs as the Overall statistics,
and the note under the Overall table says so.

The tests now also pin the grouped tables (topic and source): their
means, deltas, unpaired counts and the "(N unpaired)" cell, plus the
without-arm min/max and a delta that is not the difference of rounded
means.

Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>

* test(report): pin the pipeline summary rounding and the count cells

The pipeline summary's delta is the difference of the unrounded means, and
its baseline mean covers the complete pairs only. The eval report's Summary,
breakdown and difficulty rows print the unpaired count beside the question
count. Each of these had a surviving mutant. The new assertions kill them.

Thread: PRRT_kwDOVBg7g86wpmIH
Review: PRR_kwDOVBg7g88AAAABUqK69Q#1

Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>

* docs(stats): say which reports call paired_summary

The docstring said every report that prints arm means or a delta calls
paired_summary. The eval report, the report generator and the pipeline
summary do. The dashboard and the model-compare summary still average each
arm separately, which is follow-up work because it changes their numbers.

Thread: IC_kwDOVBg7g88AAAABgAwfHA.a
Review: PRR_kwDOVBg7g88AAAABUdomSg: already fixed in 68d90f23 and f09eeb77

Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>

* fix(report): divide the win and degradation rates by the complete pairs

The win rate and the degradation rate in Key Observations divided by every
question, but their numerators count only the complete pairs. An unpaired
question therefore lowered both rates. They now divide by the complete pairs,
the same set the Overall delta uses.

Thread: PRR_kwDOVBg7g88AAAABUbZiqA

Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>

* docs(changelog): name the other report numbers the pairing changes

The fragment did not say that the top/bottom performer tables and the
Key Observations win and degradation rates now count only the complete
pairs, nor that the dashboard and the model-compare summary keep the
per-arm means and so differ from report generate on unpaired files.

Signed-off-by: Alexander Rybkin <alexander.rybkin@intel.com>

---------

Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
Signed-off-by: Alexander Rybkin <alexander.rybkin@intel.com>
Co-authored-by: Alexander Rybkin <alexander.rybkin@intel.com>
napetrov pushed a commit to napetrov/dokimi that referenced this pull request Oct 29, 2026
…tel#286)

* fix(dashboard, report): average both arms over the complete pairs

Settles the point the review of intel#250 left for a follow-up: the dashboard
and the `report model-compare` run summary averaged each arm over its own
scored questions, while `report generate`, `report eval` and the pipeline
average both over the questions scored in both arms (paired_summary). On
an eval file with q1 80/60, q2 40/40 and q3 100/- the dashboard and
model-compare printed 73.3 / 50.0 beside a +10.0 delta; `report generate`
prints 60.0 / 50.0 / +10.0.

- dashboard/aggregator.py: the snapshot and each question-source group
  take both arm means and the delta from the pairs (_arm_means). The
  delta is the difference of the pair means, as in paired_summary, not
  the mean of the stored per-question delta.
- report/model_compare.py: _run_summary averages with_avg and
  without_avg over the same pairs as delta_avg.
- In both, a run that scored one arm only keeps that arm's mean (the
  dashboard's doc_score and the baseline-only ranking read it); with
  both arms scored but no pair, neither mean is given.
- The stats module docstring, the unreleased intel#250 fragment, the
  quickstart and the model-compare docs say so; the model-compare
  goldens and the growth baseline of _load_snapshot are updated.

Signed-off-by: Anton Volkov <antonwolfy@gmail.com>

* fix(report): model-compare difficulty rows average over the pairs

The same point as the previous commit (the review of intel#250: an arm mean
over its own scored questions beside a delta over the pairs), at its
other site in `report model-compare`: each By Difficulty row averaged
the context arm over every context-scored question in the bucket, while
the delta beside it covers the pairs only. On a bucket with q1 80/60,
q2 40/40 and q3 100/- the row printed 73.3 | +10.0; it now prints
60.0 | +10.0, the run summary's numbers on the same questions.

- report/model_compare.py: _difficulty_cells takes the context mean and
  the delta from _run_summary, so a bucket follows the overall summary's
  rule: with pairs, the pair means; context arm only, its context mean
  and no delta; both arms scored but no pair, no mean. A bucket with no
  context score still shows the baseline, as before.
- The model-compare docs and the unreleased fragment name the
  difficulty rows; the goldens (the By Difficulty context cell of eight
  sections), the growth baseline of model_compare.py and its count in
  the records guard (one condition-key read fewer) are lowered.

Signed-off-by: Anton Volkov <antonwolfy@gmail.com>

---------

Signed-off-by: Anton Volkov <antonwolfy@gmail.com>
antonwolfy added a commit that referenced this pull request Nov 1, 2026
* fix(answers): Stack P asks the doc source for top_k docs (#228 step 2)

The answerer stored top_k and never sent it, so retrieval asked the doc source for its default max_results. It now sends max_results=top_k, the same request DocTreatment makes on Stack A. With the default top_k of 3 the reranker sees 3 raw docs, not 5, so with-docs answers and their judge scores move.

Flips the PINNED rows D1 retrieval.passed and D1 retrieval.max_results. Test fakes that take the doc-source call now accept max_results.

Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>

* fix(benchmark): benchmark run takes and passes --judge-temperature (#228 step 2)

benchmark run had no judge temperature flag, so its judge always ran at the provider default while evaluate and arms run took one. The flag is added (default unset, so runs without it are unchanged) and reaches the EvaluationPipeline judge.

Flips the PINNED row D6 cli.judge_temperature_flag. benchmark batch does not get the flag yet.

Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>

* fix(pipeline): resolve the doc library id through the registry (#228)

EvaluationPipeline took the library id from --context7-id or the client's
own resolver, so a product whose products.yaml declares context7_id was
still looked up by its registry key. Resolve it with the shared
resolve_library_id_for_product helper, the same order the arms stack uses:
explicit id, then the registry's context7_id, then the client.

Question generation and answer generation both go through the helper.
The registry is injectable (registry=) and defaults to the bundled
products.yaml. Parity row D8 flips from client/resolved to registry/lib.

Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>

* fix(judge): do not score the no-retrieval placeholder by default (#228)

When no document reached the prompt (no hit, or the reranker raised), the
answerer records "[FALLBACK: No relevant documentation found]" as the
with-docs answer, and the judge scored that text like a model answer. It
inflated the with-docs side of the paired delta on questions the docs did
not answer.

The judge now leaves a context answer whose doc_source is a no-retrieval
source (NO_RETRIEVAL_SOURCES) unjudged, so its with-docs score and delta
are None and the question drops out of the paired statistics. The record
and its retrieval_hit stay. Opt back in with judge_placeholder=True, or
--judge-placeholder on benchmark run, orchestrate evaluate and eval score.

The trust gate's retrieval_hit_rate read only the both-arms-scored
evaluations. Unjudged misses would then vanish from it and an all-miss run
would read as "not measured". It now counts every evaluation's
retrieval_hit. The thresholds are unchanged.

Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>

* refactor(answerer): delete dead code in the context arm (#228)

Answerer built a self.llm client in __init__ that nothing read: the answer
call goes through llm_call_with_usage, which picks the provider's client
itself. Only the unsupported-provider ValueError is kept; the client
construction and its imports are gone.

_generate_with_docs had an "if not docs" branch that could not run, because
_generate_answer_pair calls it only when retrieval returned documents. The
branch is deleted with its unreachable "[No documentation retrieved]" record.

No parity row changes: nothing observable on either stack moved.

Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>

* feat(artifacts): stamp method_version in the answers and judge output (#228)

The Stack P answers artifact and the judge's evaluation output now carry
method_version, from defaults.ANSWERING_METHOD_VERSION (2). The steps
before this one are version 1 and have no stamp, so a later compare can
refuse to put runs of different versions side by side. compare does not
read the stamp yet.

answers.v1 and eval.v1 document the field. The arms artifact is not
stamped in this change; the new parity row records that.

Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>

* test(parity): import List for the paired-delta helper (#228)

ruff F821 flagged the annotation on _paired_deltas. The module uses
`from __future__ import annotations`, so the missing name did not fail at
import time and the parity tests passed without it.

Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>

* test(answerer): one provider allow-list smoke test replaces the init set-then-read tests (#228)

With the llm construction removed from Answerer.__init__, test_init_openai and
test_init_anthropic only set attributes and read them back, which the test
quality audit flags (set_then_read). The new parametrized test checks what
init does now: each supported provider is accepted. It is marked as a
must-not-raise smoke test.

Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>

* test(answerer): pin the top_k value at the Answerer call site (#228)

test_calls_mcp_client builds the Answerer with the default top_k, so a literal
max_results=5 would pass it. The new test builds it with top_k=3 and asserts
the doc source is asked for 3. On intel/main the same probe sends no
max_results at all, so the test fails there.

Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>

* docs(gate): say which trust-gate checks run on both-arm-scored questions (#228)

The methodology list and the method_version comment described the trust gate
and the stamp more broadly than the code does. The score checks run over the
both-arm-scored questions, so a question with no document leaves them. Nothing
reads method_version yet; the comment now says so.

Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>

* fix(gate): count unjudged retrieval misses as unusable in zero_score_fraction (#228)

A question whose retrieval missed is no longer judged with docs, so it has
no context score. The zero-score check counted only scored questions, so a
run where most questions miss passed it: 4 misses of 14 gave 0.0 PASS, where
intel/main gave 0.286 FAIL (the misses were judged at zero there).

The check now counts a miss (no context score, a baseline score, and
retrieval_hit False) as unusable, in the numerator and the denominator. The
message says such rows had no document. Thresholds, min_questions and the
early return are unchanged. The new fail-severity retrieval check is not
added here.

Regression test: tests/test_trust_gate_unjudged_misses.py. It fails on the
parent commit (4 of 14 passes at 0.0) and passes with this change.

Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>

* docs(gate): zero-score bullet counts retrieval misses; parse evaluations once (#228)

The methodology bullet still said the zero-score fraction leaves out
questions with no document. It counts them now (c517c72). Update it.

Also parse the evaluations once in evaluate_trust. The input may be a
one-shot iterable, and the old code parsed it again for the retrieval check.

Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>

* fix(judge): a judge-error row keeps the retrieval outcome of its answer

The retrieval coverage floor counted every judge-error row as an unrecorded
outcome, so a run with more than 20% judge errors lost the retrieval check.

Thread: PRRT_kwDOVBg7g86xgwoY
Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>

* fix(diagnoser): an unjudged retrieval miss is an empty retrieval

With the no-retrieval placeholder left unjudged its delta is None, and
diagnose() returned insufficient_data before it looked at the doc source.

Thread: PRRT_kwDOVBg7g86xgwog
Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>

* fix(pipeline): say how many questions the run summary left out

The summary averages the questions scored in both arms (paired_summary, #250),
so an unjudged retrieval miss is in none of the three numbers. The run result
now carries paired and unpaired, and the printed line names them.

Thread: PRRT_kwDOVBg7g86xgwon
Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>

* fix(benchmark): resolve the library id through the --registry that was loaded

benchmark run and batch loaded the registry for the entry, but the pipeline
looked the key up in the bundled products.yaml. A custom entry without a
context7_id then reached Context7 as a bare key and retrieved nothing, and a
custom key that the bundled registry also has took the bundled id.

Thread: PRRT_kwDOVBg7g86xgwou
Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>

* test(pipeline): pin the explicit id and the registry-key lookup of _library_id

Both mutants named in the review (explicit_id=None, product instead of
product_key) now fail a test.

Thread: PRRT_kwDOVBg7g86xiJYO
Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>

* fix(judge): stamp whether the placeholder was judged in the eval output

--judge-placeholder gives different numbers on the same answers under the same
method_version. The eval output now carries judge_placeholder (declared in
eval.v1), so two files of one version that are not comparable can be told apart.

Thread: PRRT_kwDOVBg7g86xgwoz
Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>

* fix(gate): show the retrieval rate when no question is scored in both arms

With every question an unjudged miss the gate returned after min_questions
with "0 questions evaluated", pointing away from the cause. The retrieval
check is now a helper and also runs on that path. The 0-hit case has its
test again, and the 2-hit test is named for what it checks.

Thread: PRRT_kwDOVBg7g86xgwo7
Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>

* test(gate): pin the three conditions of the unjudged-miss term

A legacy row, a hit that was not scored and a miss without a baseline must
leave zero_score_fraction unchanged; each mutant of the filter named in the
reviews now fails. The judge stub branch that only a judged miss reached is
gone: the default leaves misses unjudged.

Thread: PRRT_kwDOVBg7g86xiJYM
Thread: PRRT_kwDOVBg7g86xlsnp
Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>

* test(cli): drive the real handlers for --judge-temperature and --judge-placeholder

benchmark run (single and multi-run) is driven through cmd_benchmark_run with
a faked pipeline; evaluate and eval score run end to end against a mocked
judge transport. Each removed call-site kwarg named in the reviews now fails a
test. The old test named for the handler called _run_single_library directly.

Thread: PRRT_kwDOVBg7g86xiJYT
Thread: PRRT_kwDOVBg7g86xlsUD
Thread: PRRT_kwDOVBg7g86xlsb0
Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>

* docs(changelog): name the flags and commands that exist; say old scores differ

benchmark run has no --top-k and there is no orchestrate evaluate: the command
is dokimi evaluate. The fragment now says what each command asks for, that
answers generate changes too, that old scores are not comparable, and covers
the fixes of this round.

Thread: PRRT_kwDOVBg7g86xgwo3
Thread: PRRT_kwDOVBg7g86xlsKV
Thread: PRR_kwDOVBg7g88AAAABU4c24A
Thread: PRR_kwDOVBg7g88AAAABU5LNng
Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>

* test(answerer): drop the ChatOpenAI patch the module no longer uses

dokimi.eval.answerer neither imports nor uses ChatOpenAI; the patch with
create=True and its mock did nothing and suggested the init builds a client.

Thread: IC_kwDOVBg7g88AAAABg14M_w
Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>

* chore(pipeline): drop the records imports that the merge with main left unused

main's paired_summary (#250) replaced the BASELINE/CONTEXT/records reads in
run(); the merge kept this branch's import line, which ruff F401 rejects.

Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>

* test(pipeline): pin the library id the question step hands the topic extractor

Codecov flagged the one patch line in _generate_questions that no test reached.
The test asserts the extractor gets the registry's declared id; reverting the
line to the client's name lookup fails it.

Thread: IC_kwDOVBg7g88AAAABgqSv4w
Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>

* test: pin the branches the #274 review nits left untested

Three nits from the review of #274, one commit:

- test_diagnoser: an unjudged `none` retrieval miss is an empty retrieval,
  and an errored placeholder answer stays insufficient_data. Deleting the
  `not error` guard now fails a test.
- test_pipeline_library_id: a product key that is not in the registry is
  resolved by the client under the key. Passing the display name instead
  now fails a test.
- test_pipeline_summary: an all-paired run does not print "left out".
  Printing the suffix unconditionally now fails a test.

No source change: each new test was checked against the mutation the nit
describes (red), then the unchanged tree (green).

Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>

* docs(changelog): say benchmark run --judge-temperature is a new flag

The flag did not exist before this PR: benchmark run rejected it as an
unknown argument. The entry moves it from Changed and Fixed to Added, so
the Fixed section no longer repeats the claim.

Thread: PRRT_kwDOVBg7g86xtZgo
Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>

* docs(changelog): say a judge-error row on a retrieval miss counts in zero_score_fraction

A judge-error row whose retrieval missed now counts as unusable in the
zero-score check; one on a retrieval hit is still left out. The fragment
did not say so, and the check is fail-severity.

Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>

* docs: keep the comment blocks this PR adds to two lines

Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>

* fix(gate): a judge error on a retrieval miss counts as unusable

The zero-score check counted an unjudged retrieval miss, but a judge error
on a miss has no scores either and was left out. The error row is still a
miss, so it is counted; a judge error on a hit stays out, as before. Told
apart by the record's error field, not by its score shape.

Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>

* fix(answerer): a missing top_k asks the doc source for the default

Answerer passed max_results=self.top_k to the doc source, so a None top_k
reached the client as max_results=None, which returns every chunk. Make the
Answerer default and an explicit None both DEFAULT_RETRIEVAL_TOP_K (3). The
answers command already resolves --top-k to that default before it gets here;
this closes the direct-construction path.

Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>

* docs(pipeline): document registry and judge_placeholder in Args

Signed-off-by: Anton Volkov <antonwolfy@gmail.com>

* docs(changelog): limit what --judge-placeholder restores

Signed-off-by: Anton Volkov <antonwolfy@gmail.com>

* docs(methodology): zero-score bullet counts judge-error misses

Signed-off-by: Anton Volkov <antonwolfy@gmail.com>

* style(answerer): one import from dokimi.defaults

Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>

* test(trust-gate): pin the scored-miss clause of the unjudged-miss filter

A miss the judge scored with a 0 context score is already a zero; a
mutant that drops the score-is-None clause counts it twice and passes
the existing tests. The new case asserts 1/11, and fails on that mutant.

Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>

* test(answerer): pin a non-default top_k so an override cannot be ignored

The explicit top_k case used 3, which is also the default. With the
constructor ignoring the caller, the suite still passed. The case now
uses 7 and expects max_results=7; the mutant fails it.

Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>

* docs(methodology): name the both-arm subset in the three metric bullets

The zero-score fraction uses an expanded subset; the minimum with_docs
average, the average delta and the coefficient of variation do not.

Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>

* test(cli): allowlist --judge-placeholder, which this stack adds

No registry concept covers the placeholder judge yet, so the spelling
gets a reasoned allowlist row, as the contract test asks.

Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>

* docs(cli): regenerate the CLI snapshot after the merge of main and this stack's flags

Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>

* fix(eval): eval score stamps the method version of the answers it judges

An answers file with no method_version is legacy (version 1). Judging it used to stamp the eval artifact with the current version, so legacy answers re-judged today read as version 2 next to answers generated under the new method. The Judge now takes the answers' version; answers generated in process keep the current one.

Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>

* fix(pipeline): an mcp source's ;id= outranks the registry's context7_id

resolve_library_id_for_product checks the registry before the client, so a registered product replaced the id named in an mcp: descriptor. The pipeline now passes the descriptor's id as the explicit id, after the --context7-id option and before the registry, as it did before the registry lookup.

Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>

* docs(changelog,methodology): say what the stamp and the zero-score bullet cover

Legacy artifacts are version 1 but readers do not normalize that yet; the zero-score bullet names the case where no question is scored in both arms.

Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>

* fix(pipeline): eval judges answers with the method version of their artifact

The pipeline built its Judge without answers_method_version, so answers
read from a version 1 artifact, or from an unstamped legacy file, were
stamped as version 2. The version now comes from the artifact it read;
fresh in-memory answers keep the current version.

Thread: PRRT_kwDOVBg7g86yJ-x3
Comment: IC_kwDOVBg7g88AAAABhDPhBg
Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>

* test(trust-gate): expect the zero-score wording the gate now uses

Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>

* Address review: a judged placeholder with no baseline is insufficient data

With judge_placeholder the context answer is scored, so a None delta
comes from a missing baseline. diagnose() classed any no-retrieval
context answer with no delta as an empty retrieval; it now does so only
when the context was left unjudged.

Thread: PRRT_kwDOVBg7g86yMEiW
Thread: IC_kwDOVBg7g88AAAABhDPhBg
Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>

* Address review summaries: points already fixed on this branch

Each review below raised points that its inline threads cover; the fixes
are in the commits that answer those threads (the gate clauses are pinned
by test_trust_gate.py and test_trust_gate_unjudged_misses.py).

Thread: PRR_kwDOVBg7g88AAAABU4c24A: already fixed in the inline threads' commits
Thread: PRR_kwDOVBg7g88AAAABU5LNng: already fixed in the inline threads' commits
Thread: PRR_kwDOVBg7g88AAAABVF59Bw: already fixed in the inline threads' commits
Thread: PRR_kwDOVBg7g88AAAABVGGmvw: already fixed in 5fae382, 6dc1f42 and 805aa58
Thread: PRR_kwDOVBg7g88AAAABVHLzTg: already fixed in the inline threads' commits
Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>

* Refuse a CLI judge's temperature in benchmark run before any answer

The judge refuses a temperature for claude-cli, but only after
benchmark run had generated every answer. benchmark run and batch
now run the same check first, exit 2, as dokimi run does.

Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>

* Reword the unjudged-miss comment in diagnose()

Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>

* fix(gate): keep stored judge-error rows out of the retrieval coverage count

Eval files written before error rows carried retrieval_hit turned a measured
hit rate into "Not measured" (and a failing rate into a pass) once the
denominator became every evaluation. Such a row now counts as neither
measured nor unrecorded, as before; new error rows still count.

Signed-off-by: Alexander Rybkin <alexander.rybkin@intel.com>

* fix(answers): refuse a top_k below 1 now that it reaches the doc source

top_k is sent as max_results, so answers generate --top-k 0 (or a config
retrieval.top_k of 0, "3" or true) made every answer an unjudged placeholder
and still exited 0. The flag takes positive_int, as dokimi evaluate does, and
a bad config value exits 2 before any question is answered.

Signed-off-by: Alexander Rybkin <alexander.rybkin@intel.com>

* fix(eval): refuse a malformed answers method_version before judging

eval score and the pipeline copied the answers' method_version into the eval
unchecked: "2", true or 3.5 failed eval validation after judge calls were
paid for and left no file, 0 became 1 and -7 went through. A stamp that is
not a positive int now exits 2 (eval score) before any judge call.

Signed-off-by: Alexander Rybkin <alexander.rybkin@intel.com>

* fix(pipeline): key the topics cache on the doc source too

The library id now comes from the registry for local:, url: and okf:
sources as well, and .cache/topics was keyed on that id alone, so a second
doc set of one registered product silently got the first set's topics.

Signed-off-by: Alexander Rybkin <alexander.rybkin@intel.com>

* fix(dashboard): score a no-error retrieval miss as context 0

A question the context arm never answered, with no retrieval hit and a baseline score,
counted as no score, so the snapshot averaged the baseline alone (0 hits gave doc_score
equal to the baseline). It now counts as 0.0 for the context arm.

Thread: IC_kwDOVBg7g88AAAABhdSHJQ.a
Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>

* fix(report): say when there are no complete pairs

With no question scored in both arms, report eval printed a 0.0 arm mean, an
Overall delta of +0.0 and a delta over 0 pairs. The Summary cells and the Overall delta
line now show an em dash and a note that there are no complete pairs.

Thread: IC_kwDOVBg7g88AAAABhdSHJQ.b
Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>

* docs(changelog): an absent or null method_version is version 1

The changelog said a non-positive-integer stamp is refused; a null or absent one is the
legacy version 1 and is accepted.

Thread: PRRT_kwDOVBg7g86yPRma
Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>

* docs(methodology): qualify the trust-gate question and denominator rules

The minimum question count excludes an unjudged question only for the single Judge; a
panel judges a fallback-only answer. The zero-score denominator adds retrieval misses with
an unscored context arm and either a scored baseline or a judge error, and no other miss.

Thread: PRRT_kwDOVBg7g86yPRmb
Thread: PRRT_kwDOVBg7g86yPRmc
Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>

* docs(pr): PR body names the legacy stamp and the 14-of-14 retrieval row

No code change. The body edit (line 22: method_version 1 for an eval of legacy answers;
the 14-of-14 row: retrieval_hit_rate 0.0 FAIL) is applied to the PR description.

Thread: IC_kwDOVBg7g88AAAABhdSHJQ.c
Thread: IC_kwDOVBg7g88AAAABhdSHJQ.d
Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>

* fix(report): the unpaired note says there are no complete pairs

With no complete pair, the note under the Summary still said the means were over 0
complete pairs. It now says there are no complete pairs and that no arm mean or delta is
given.

Thread: IC_kwDOVBg7g88AAAABhdSHJQ.b
Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>

* refactor(dashboard, report): extract helpers to keep the complexity ratchet

The miss-as-zero rule and the no-pairs lines pushed _load_snapshot over the
cap of 10 and generate_report past its baseline of 18. Move them into
_context_score, _overall_line and _delta_summary; no behavior change.

Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>

* fix(dashboard): leave an unjudged miss out of the arm means and delta

A no-error retrieval miss scores 0 as the context arm, so the doc score
counts it, but it is no judged measurement: pairing it with its baseline
made the dashboard report a -20 delta where the eval report gave +20 over
the same file. The arm means and delta are now over the measured pairs;
the doc health (doc_score, status) still counts the miss as 0.

Thread: PRRT_kwDOVBg7g86yUBEO
Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>

* fix(report): print no delta for a set with no complete pair

A set with no complete pair printed 0.0 for its context, baseline and delta
in the Dynamic vs Static and difficulty tables, and the agreement bullet
claimed consistency. Those rows now print an em dash like the Summary, and
the agreement bullet says there is no delta when either set has no pair.

Thread: PRRT_kwDOVBg7g86yUBES
Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>

* fix(report): no zero-mean delta or conclusion for an all-miss run

A run with no complete baseline/skill pair printed zero means, a zero
delta and a conclusion. Stats now carry None without a pair; the
overall, cluster and source rows show an em dash, the advice is
suppressed, and the unpaired note explains that no pairs were made.
Also lower the _format_markdown growth baseline to its new complexity.

Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>

* Test that Judge diagnoses a judged placeholder without a baseline

Signed-off-by: Anton Volkov <antonwolfy@gmail.com>

* Name the report changes in the Stack P step 2 changelog

Signed-off-by: Anton Volkov <antonwolfy@gmail.com>

* test(report): pin the no-pair and miss guards the review found untested

- A group with no complete pair has no means in the grouped tables (topic and source), in JSON or markdown.
- A retrieval miss counts as a context zero only with a clean baseline score: no baseline score, a top-level error and a with_docs error each leave the doc score alone.
- methodology: the single Judge skips the no-retrieval placeholder unless --judge-placeholder is set; JudgePanel judges nonempty fallback answers.

Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>

* docs(changelog): qualify the miss-as-zero and stamping claims

The doc score counts a miss as 0 only with a baseline score and no error
on either arm, and panel-score artifacts carry neither method_version nor
judge_placeholder. Say so where the entry claims otherwise.

Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>

---------

Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
Signed-off-by: Anton Volkov <antonwolfy@gmail.com>
Signed-off-by: Alexander Rybkin <alexander.rybkin@intel.com>
Co-authored-by: Anton Volkov <antonwolfy@gmail.com>
Co-authored-by: Alexander Rybkin <alexander.rybkin@intel.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants